import buffer from '@ohos.buffer';
import { computeCode } from './code';
import { Flags } from './models/flags';
import { convertV3TokenDate } from './date';
import { newToken } from './models/newToken';
import { sha256Hash, sha256Hmac, aes256CBCDecrypt, PBKDF2Sha256 } from './aes';

export async function v3(rawToken: string, password: string = '', deviceId: string = '') {
    return await decrypt(parse(rawToken), password, deviceId);
}

async function decrypt(token: newToken, password: string = '', deviceId: string = '') {
    if (!password && token.flags.passwordIsRequired) throw new Error('Missing password');
    if (!deviceId && token.flags.deviceIdIsRequired) throw new Error('Missing deviceId');
    const deviceIdClean = deviceId.replace(new RegExp('[^\da-f]', 'gi'), "").toUpperCase();
    let hash = computeHash('', deviceIdClean, token.nonce);
    if (hash.compare(token.nonceDevIdHash) != 0) throw new Error('Mismatching _nonce_devid_hash');
    hash = computeHash(password, deviceIdClean, token.nonce);
    if (hash.compare(token.nonceDevIdPassHash) != 0) throw new Error('Mismatching _nonce_devid_pass_hash');
    const hmacKey = await deriveKey(password, deviceIdClean, token.nonce, 0);
    hash = await sha256Hmac(hmacKey.buffer, toBytes(token, false).buffer);
    if (hash.compare(token.mac) != 0) throw new Error('Mismatching _mac');
    hash = await deriveKey(password, deviceIdClean, token.nonce, 1);
    let payload = await aes256CBCDecrypt(hash.subarray(0, 32).buffer, token.encPayload.buffer, token.nonce.buffer);
    if (payload.length < 160) throw new Error('Payload too short');
    let payloadIndex = 0;
    token.serial = payload.toString('utf8', payloadIndex, 12).trim();
    payloadIndex += 16;
    token.decryptedSeed = payload.subarray(payloadIndex, payloadIndex + 16);
    payloadIndex += 18;
    const flags = token.flags;
    flags.mode = payload[payloadIndex++] > 0;
    token.digits = payload[payloadIndex++];
    flags.pinIsRequired = payload[payloadIndex++] != 0x1f;
    if (payload[payloadIndex] != 30 && payload[payloadIndex] != 60) throw new Error('Invalid interval')
    token.intervalInSeconds = payload[payloadIndex++] == 30 ? 30 : 60;
    payloadIndex += 2;
    token.createdAt = convertV3TokenDate(payload.subarray(payloadIndex, payloadIndex + 5));
    payloadIndex += 8;
    token.expiresAt = convertV3TokenDate(payload.subarray(payloadIndex, payloadIndex + 5));
    flags.usesAppDerivedSeeds = true;
    flags.usesTimeDerivedSeeds = false;
    flags.keyIs128Bit = true;
    token.computeCode = computeCode.bind(null, token);
    return token;
}

function parse(rawToken: string): newToken {
    const token = decodeURIComponent(rawToken.split('ctfData=')[1]);
    const data = buffer.from(token, 'base64');
    if (data.length < 291) throw new Error('Token too short');
    let dataIndex = 0;
    let version: number = data[dataIndex++];
    if (version != 3) throw new Error('Expected token version 3 but got ' + version);
    let flags: Flags;
    flags.passwordIsRequired = (data[dataIndex++] > 0);
    flags.deviceIdIsRequired = (data[dataIndex++] > 0);
    let nonceDevIdHash = data.subarray(dataIndex, dataIndex + 32);
    dataIndex += 32;
    let nonceDevIdPassHash = data.subarray(dataIndex, dataIndex + 32);
    dataIndex += 32;
    let nonce = data.subarray(dataIndex, dataIndex + 16);
    dataIndex += 16;
    let encPayload = data.subarray(dataIndex, dataIndex + 176);
    dataIndex += 176;
    let mac = data.subarray(dataIndex, dataIndex + 32);
    return {
        version,
        nonceDevIdHash,
        nonceDevIdPassHash,
        nonce,
        encPayload,
        mac,
        flags
    }
}

function computeHash(password: string = '', deviceId: string = '', salt: buffer.Buffer) {
    if (!salt) throw new Error('Missing salt');
    const hash_buf = buffer.alloc(salt.length + 48 + password.length, 0);
    salt.copy(hash_buf, 0, 0, salt.length);
    if (deviceId) hash_buf.write(deviceId, salt.length, 48);
    if (password) hash_buf.write(password, salt.length + 48, password.length);
    return sha256Hash(hash_buf.buffer);
}

function toBytes(token: newToken, includeMac: boolean) {
    let iPos = 0;
    const tokenBytes = buffer.alloc(3 + token.nonceDevIdHash.length + token.nonceDevIdPassHash.length + token.nonce.length + token.encPayload.length + (includeMac ? token.mac.length : 0));
    tokenBytes[iPos++] = token.version;
    tokenBytes[iPos++] = token.flags.passwordIsRequired ? 1 : 0;
    tokenBytes[iPos++] = token.flags.deviceIdIsRequired ? 1 : 0;
    token.nonceDevIdHash.copy(tokenBytes, iPos, 0, token.nonceDevIdHash.length);
    iPos += token.nonceDevIdHash.length;
    token.nonceDevIdPassHash.copy(tokenBytes, iPos, 0, token.nonceDevIdPassHash.length);
    iPos += token.nonceDevIdPassHash.length;
    token.nonce.copy(tokenBytes, iPos, 0, token.nonce.length);
    iPos += token.nonce.length;
    token.encPayload.copy(tokenBytes, iPos, 0, token.encPayload.length);
    iPos += token.encPayload.length;
    if (includeMac) {
        token.mac.copy(tokenBytes, iPos, 0, token.mac.length);
        iPos += token.mac.length;
    }
    return tokenBytes;
}

function deriveKey(pass: string = '', devid: string = '', salt: buffer.Buffer, keyId: 0 | 1) {
    if (keyId < 0 || keyId > 1) throw new Error("keyId must be 0 or 1.");
    if (!salt) throw new Error('Missing salt');
    let i: number;
    const key0 = [0xd0, 0x14, 0x43, 0x3c, 0x6d, 0x17, 0x9f, 0xeb, 0xda, 0x09, 0xab, 0xfc, 0x32, 0x49, 0x63, 0x4c];
    const key1 = [0x3b, 0xaf, 0xff, 0x4d, 0x91, 0x8d, 0x89, 0xb6, 0x81, 0x60, 0xde, 0x44, 0x4e, 0x05, 0xc0, 0xdd];
    const key = buffer.from((keyId > 0) ? key1 : key0);
    const buf0 = buffer.alloc(48 + key.length + salt.length + pass.length, 0);
    const buf1 = buffer.alloc(buf0.length / 2);
    if (pass) buf0.write(pass);
    if (devid.length > 0) buf0.write(devid, pass.length, 48);
    key.copy(buf0, pass.length + 48, 0, key.length);
    salt.copy(buf0, pass.length + 48 + key.length, 0, salt.length);
    // PBKDF2 password is "every 2nd byte of the input"
    for (i = 1; i < buf0.length; i += 2)
        buf1[i >> 1] = buf0[i];
    return PBKDF2Sha256(32, buf1.buffer, salt.buffer, 1000);
}